"""
@Description :
逻辑回归算法实现
@Author : inY_Yue
@Time : 2022/4/27 - 11:22
"""
import numpy as np


class LogisticRegression:
    """
    @ClassName :LogisticRegression
    @Description :
    逻辑回归基于梯度下降实现
    @Author :inY_Yue
    """

    def __init__(self, n_iter=200, eta=1e-3, tol=None):
        """
        @Description :
        初始化函数
        @Params :
        n_iter - 迭代次数
        eta - 学习率
        tol - 精确度
        """
        self.n_iter = n_iter
        self.eta = eta
        self.tol = tol
        self.w = None  # 模型参数矩阵
        self.predict_probas = []

    def _z(self, X, w):
        """
        @Description :
        g(x) 函数，计算x 与 w 的内积
        @Params :
        X - 数据
        w - 参数
        @Returns :
        X与w 的内积
        """
        return np.dot(X, w)

    def _sigmoid(self, z):
        """
        @Description :
        sigmoid 计算函数
        @Params :
        z - 变量
        @Returns :
        sigmoid 计算的结果值
        """
        return 1. / (1. + np.exp(-z))

    def predict_proba(self, X):
        """
        TODO:
        给定待测数据集，返回表示X_predict的结果概率向量
        """
        return self.predict_probas

    def _predict_proba(self, X, w):
        """
        @Description :
        h(x)函数，预测为正例的概率
        @Params :
        X - 数据
        w -参数
        @Returns :
        概率值
        """
        z = self._z(X, w)
        self.predict_probas.append(z)
        return self._sigmoid(z)

    def _loss(self, y, y_proba):
        """
        @Description :
        计算损失
        @Params :
        y - 实际值
        y_proba - 概率
        @Returns :

        """
        m = y.size
        p = y_proba * (2 * y - 1) + (1 - y)
        return -np.sum(np.log(p)) / m

    def _gradient(self, X, y, y_proba):
        """
        @Description :
        计算梯度
        @Params :
        X,y,y_proba
        @Returns :
        计算结果
        """
        return np.matmul(y_proba - y, X) / y.size

    def _gradient_descent(self, w, X, y):
        """
        @Description :
        梯度下降实现过程
        @Params :
        w - 参数矩阵
        X - 数据值
        y - 实际值
        @Returns :
        """
        # 如果用户指定tol，则启用早期停止法
        if self.tol is not None:
            loss_old = np.inf

        self.loss_list = []

        # 使用梯度下降，至多迭代 n_iter 次，更新 w
        for step_i in range(self.n_iter):
            y_proba = self._predict_proba(X, w)  # 预测所有点为 1 的概率
            loss = self._loss(y, y_proba)  # 计算损失
            self.loss_list.append((loss))

            # 早期停止法
            if self.tol is not None:
                if loss_old - loss < self.tol:  # 如果损失小于阈值，则终止迭代
                    break
                loss_old = loss

            grad = self._gradient(X, y, y_proba)  # 计算梯度
            w -= self.eta * grad  # 更新参数w

    def _preprocess_data_X(self, X):
        """
        @Description :
        数据预处理
        @Params :
        X - 原始数据
        @Returns :
        X_ - 处理之后的数据
        """
        # 拓展X,添加 x0 并且设置为1
        m, n = X.shape
        X_ = np.empty((m, n + 1))
        X_[:, 0] = 1
        X_[:, 1:] = X
        return X_

    def train(self, X_train, y_train):
        """
        @Description :
        进行训练
        @Params :
        X_train - 训练数据
        y_train - 训练数据标签
        @Returns :
        直接引起 参数w 的变化
        """
        # 预处理X_train
        X_train = self._preprocess_data_X(X_train)

        # 初始化参数向量
        _, n = X_train.shape
        self.w = np.random.random(n) * 0.05

        # 执行梯度下降训练
        self._gradient_descent(self.w, X_train, y_train)

    def predict(self, X):
        """
        @Description :
        预测
        """
        # 预处理X_test
        X = self._preprocess_data_X(X)
        # 预测为正例子的概率
        y_pred = self._predict_proba(X, self.w)

        # 根据概率预测类别
        return np.where(y_pred >= 0.5, 1, 0)
